﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Text;
using System.Windows.Xps.Packaging;

using Ionic.Zip;

namespace FirstFloor.Documents.Services.Storage
{
    /// <summary>
    /// Provides the shared implementation of a document store.
    /// </summary>
    public abstract class DocumentStoreBase
        : IDocumentStore
    {
        /// <summary>
        /// Adds specified document to the store.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <param name="name">The name.</param>
        public abstract void AddDocument(string name, Stream document);

        /// <summary>
        /// Determines whether the specified document exists.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns>
        /// 	<c>true</c> if the specified document exists; otherwise, <c>false</c>.
        /// </returns>
        public abstract bool ContainsDocument(string name);

        /// <summary>
        /// Gets the document's last modification date.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public abstract DateTime GetDocumentLastModified(string name);

        /// <summary>
        /// Reads the document info from given document stream.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="stream">The stream.</param>
        /// <returns></returns>
        public DocumentInfo ReadDocumentInfo(string name, Stream stream)
        {
            using (var package = Package.Open(stream)) {
                var document = new XpsDocument(package);

                var info = new DocumentInfo(name);
                info.Title = document.CoreDocumentProperties.Title;
                info.Author = document.CoreDocumentProperties.Creator;
                info.PageCount = (from fixedDoc in document.FixedDocumentSequenceReader.FixedDocuments
                                  select fixedDoc.FixedPages.Count).Sum();
                if (document.Thumbnail != null) {
                    info.ThumbnailUri = document.Thumbnail.Uri;
                }

                return info;
            }
        }

        /// <summary>
        /// Retrieves the part from specified document stream.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <param name="partName">Name of the part.</param>
        /// <returns></returns>
        /// <remarks>
        /// Note: it is the responsibility of the caller to properly close the returned part stream.
        /// </remarks>
        public DocumentPartInfo GetDocumentPart(Stream document, string partName)
        {
            DocumentPartInfo result = null;

            // partName must start with /
            if (!partName.StartsWith("/")) {
                partName = "/" + partName;
            }

            if (partName == "/[Content_Types].xml") {
                // according to Open Package Conventions specification, /[Content_Types].xml is not a package part,
                // read this part using 3rd party ZIP reader and use the contenttype 'text/xml'
                using (var file = ZipFile.Read(document)) {
                    var entry = file[partName];
                    Stream stream;
                    if (entry != null) {
                        stream = new MemoryStream();
                        entry.Extract(stream);
                        stream.Seek(0, SeekOrigin.Begin);
                    }
                    else {
                        stream = MergeFragmentedPart(file, partName);
                    }
                    if (stream != null) {
                        result = new DocumentPartInfo {
                            PartName = "[Content_Types].xml",
                            ContentType = "text/xml",
                            Stream = stream
                        };
                    }
                }
            }
            else {
                // use the Open Packaging API to retrieve package parts
                var package = Package.Open(document);
                var partUri = PackUriHelper.CreatePartUri(new Uri(partName, UriKind.Relative));
                if (package.PartExists(partUri)) {
                    var part = package.GetPart(partUri);

                    result = new DocumentPartInfo {
                        PartName = part.Uri.ToString(),
                        ContentType = part.ContentType,
                        Stream = part.GetStream(FileMode.Open)
                    };

                    // ensure part name does not contain slash prefix
                    if (result.PartName.StartsWith("/")) {
                        result.PartName = result.PartName.Substring(1);
                    }

                    // if part is not a relationship itself, collect its required resources
                    if (part.ContentType != "application/vnd.openxmlformats-package.relationships+xml") {
                        result.RequiredPartNames = (from relationship in part.GetRelationshipsByType("http://schemas.microsoft.com/xps/2005/06/required-resource")
                                                    where relationship.TargetMode == TargetMode.Internal
                                                    select PackUriHelper.ResolvePartUri(relationship.SourceUri, relationship.TargetUri).ToString()).ToArray();
                    }
                }
            }

            return result;
        }

        private Stream MergeFragmentedPart(ZipFile zipFile, string partName)
        {
            var result = new MemoryStream();
            var i = 0;
            var lastPiece = false;

            while (true) {
                var pieceName = string.Format("{0}/[{1}].piece", partName, i++);
                var entry = zipFile[pieceName];
                if (entry == null) {
                    pieceName = string.Format("{0}/[{1}].last.piece", partName, i - 1);
                    entry = zipFile[pieceName];
                    lastPiece = true;
                }
                if (entry != null) {
                    entry.Extract(result);

                    if (lastPiece) {
                        break;
                    }
                }
                else {
                    break;
                }
            }
            if (result.Length != 0) {
                result.Seek(0, SeekOrigin.Begin);
                return result;
            }
            return null;
        }

        /// <summary>
        /// Gets all documents contained in this store.
        /// </summary>
        /// <returns></returns>
        public abstract IEnumerable<DocumentInfo> GetDocuments();

        /// <summary>
        /// Opens the specified document for reading.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public abstract Stream OpenDocument(string name);

        /// <summary>
        /// Removes specified document from the store.
        /// </summary>
        /// <param name="name">The name.</param>
        public abstract void RemoveDocument(string name);
    }
}
